class JSVerlet {
  constructor(canvas, options) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.ctx.lineWidth = 1;

    this.width = canvas.width;
    this.height = canvas.height;
    this.options = options || {};

    this.constraints = [];
    this.points = [];
    this.draw_points = [];

    this.mouse = new Vector();
    this.gravity = this.options.gravity || new Vector(0, 0.98);
    this.point_size = this.options.point_size || 2;
    this.show_stress = this.options.show_stress;

    this.key = {
      ctrl: false,
      alt: false,
    };

    canvas.oncontextmenu = (e) => {
      e.preventDefault();
    };

    if (this.options.edit) {
      document.onkeydown = (e) => {
        if (e.keyCode == 17) {
          this.key.ctrl = true;
        } else if (e.keyCode == 16) {
          this.draw_points = [];
        } else if (e.keyCode == 18) {
          this.key.alt = true;
        }
      };

      document.onkeyup = (e) => {
        if (e.keyCode == 17) {
          this.key.ctrl = false;
          this.draw_points = [];
        } else if (e.keyCode == 18) {
          this.key.alt = false;
        }
      };
    }

    if (this.options.edit || this.options.drag) {
      canvas.onmousedown = (e) => {
        const rect = this.canvas.getBoundingClientRect();
        this.mouse.x = e.clientX - rect.left;
        this.mouse.y = e.clientY - rect.top;
        this.mouse.down = true;

        if (this.options.edit) {
          if (e.which == 3) {
            if (this.getMousePoint()) {
              this.removePoint(this.getMousePoint());
            }
          } else {
            if (this.key.ctrl) {
              let p = this.getMousePoint();

              if (!p) {
                p = new Point(this.mouse.x, this.mouse.y, this.key.alt);
                this.points.push(p);
              }

              if (this.draw_points.length) {
                this.constraints.push(
                  new Constraint(
                    p,
                    this.draw_points[this.draw_points.length - 1]
                  )
                );
              }

              this.draw_points.push(p);
            } else if (this.options.drag) {
              this.mouse_point = this.getMousePoint();
            }
          }
        } else if (this.options.drag) {
          this.mouse_point = this.getMousePoint();
        }
      };

      canvas.onmouseup = (e) => {
        this.mouse.down = false;
        this.mouse_point = null;
      };

      canvas.onmousemove = (e) => {
        const rect = this.canvas.getBoundingClientRect();
        this.mouse.x = e.clientX - rect.left;
        this.mouse.y = e.clientY - rect.top;
      };
    }
  }

  draw(ctx) {
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    let i = this.constraints.length;
    while (i--) this.constraints[i].draw(ctx, this.show_stress);

    i = this.points.length;
    while (i--) this.points[i].draw(ctx, this.point_size);

    if (this.mouse_point) {
      ctx.beginPath();
      ctx.arc(
        this.mouse_point.pos.x,
        this.mouse_point.pos.y,
        this.point_size * 3,
        0,
        Math.PI * 2
      );
      ctx.fillStyle = "rgba(255, 255, 255, 0.2)";
      ctx.fill();

      ctx.beginPath();
      ctx.arc(
        this.mouse_point.pos.x,
        this.mouse_point.pos.y,
        this.point_size,
        0,
        Math.PI * 2
      );
      ctx.fillStyle = this.mouse_point.fixed ? "#EDEA26" : "#aaa";
      ctx.fill();
    }

    if (this.draw_points.length) {
      const point = this.draw_points[this.draw_points.length - 1];

      ctx.beginPath();
      ctx.arc(point.pos.x, point.pos.y, this.point_size * 3, 0, Math.PI * 2);
      ctx.fillStyle = "rgba(255, 255, 255, 0.2)";
      ctx.fill();

      ctx.beginPath();
      ctx.arc(point.pos.x, point.pos.y, this.point_size, 0, Math.PI * 2);
      ctx.fillStyle = "#aaa";
      ctx.fill();
    }
  }

  update(iter = 6) {
    if (this.key.ctrl) return;

    const delta = 1 / iter;

    let n = iter;
    while (n--) {
      if (this.mouse_point) {
        this.mouse_point.pos.x +=
          (this.mouse.x - this.mouse_point.pos.x) / iter;
        this.mouse_point.pos.y +=
          (this.mouse.y - this.mouse_point.pos.y) / iter;
      }

      let i = this.points.length;
      while (i--) {
        const point = this.points[i];
        point.addForce(this.gravity);
        point.update(delta);
        point.checkWalls(0, 0, this.width, this.height);
      }

      i = this.constraints.length;
      while (i--) this.constraints[i].resolve();
    }
  }

  removePoint(point) {
    let i = this.constraints.length;
    while (i--) {
      const constraint = this.constraints[i];
      if (constraint.p1 == point || constraint.p2 == point) {
        this.constraints.splice(this.constraints.indexOf(constraint), 1);
      }
    }

    if (this.points.indexOf(point) != -1) {
      this.points.splice(this.points.indexOf(point), 1);
    }
  }

  getMousePoint() {
    let closest;
    let i = this.points.length;
    while (i--) {
      const point = this.points[i];
      if (point.pos.distance(this.mouse) < 10) {
        closest = point;
      }
    }

    return closest;
  }

  addPoint(x, y, fixed) {
    const point = new Point(x, y, fixed);
    this.points.push(point);
    return point;
  }

  addConstraint(p1, p2) {
    this.constraints.push(new Constraint(p1, p2));
  }

  addShape(shapes) {
    if (!(shapes instanceof Array)) {
      if (!(shapes instanceof Shape)) {
        console.log("Error: shape is not an instance of Shape.");
        return false;
      }

      this.points = this.points.concat(shapes.points);
      this.constraints = this.constraints.concat(shapes.constraints);

      return true;
    }

    let i = shapes.length;
    while (i--) {
      const shape = shapes[i];
      if (!(shape instanceof Shape)) {
        console.log("Error: shape[" + i + "] is not an instance of Shape.");
        return false;
      }

      this.points = this.points.concat(shape.points);
      this.constraints = this.constraints.concat(shape.constraints);
    }

    return true;
  }
}
